classdef dotsAllObjectManagers < dotsAllSingletonObjects
    % @class dotsAllObjectManagers
    % Superclass for singleton objects that coordinate the behaviors of
    % other objects.
    % @details
    % dotsAllObjectManagers provides a uniform way to coordinate the
    % behaviors of multiple "managed" objects.  dotsAllObjectManagers makes
    % a contract with the dotsAllManagedObjects superclass.
    % @details
    % <b>Division of Labor</b>
    % @details
    % Snow Dots distinguishes between individual object behaviors and the
    % coordination of behaviors to form a system.  This allows for a
    % division of labor: some classes focus narrowly on behaviors such as
    % computing stimuli or checking for input, while other classes deal
    % with high-level coordination such as presenting multiple stimuli at
    % at once or delegating stimulus behaviors to a separate machine.
    % @details
    % The dotsAllManagedObjects and dotsAllObjectManagers superclasses
    % make this division of labor explicit: dotsAllManagedObjects
    % sublcasses do specific tasks and dotsAllObjectManagers subclasses
    % coordinate them.
    % @details
    % This division of labor should facilitate development of new
    % behaviors.  For example, a new stimulus class only needs to extend
    % dotsAllManagedObjects or an existing stimulus class and add some
    % novel behaviors.  It does not need to re-solve problems that were
    % already solved by a manager, such as how to present multiple stimuli
    % at once, whether to present them on a remote machine, etc.
    % @details
    % <b>Contract</b>
    % @details
    % A manager and a managed object don't need to know the details of
    % what the other is doing, but they do need to collaborate.  Thus, they
    % agree to a contract.  The contract includes some object properties:
    %   - A managed object must have a "UID" property to which the manager
    %   can assign a unique identifier.
    %   - A managed object must have a "manager" property which refers to
    %   its manager.
    %   - A managed object must have a "groups" property to which the
    %   manager can assign group names.
    %   - A managed object may have properties that are SetObservable (see
    %   "Property Attributes" in the Matlab help browser).  If it does, it
    %   must report to its manager when any of these properties changes.
    %   - An object manager must have a "managedObjects" property, which is
    %   a topsGroupedList of objects being managed.
    %   .
    % dotsAllManagedObjects and dotsAllObjectManagers satisfy the
    % object property parts of the contract, so most new classes should
    % extend one of these superclasses or a related subclass.
    % @details
    % The contract also includes some usage conventions:
    %   - Users should not create managed objects with class constructors
    %   directly.  Instead, users should use the newObject() method of an
    %   object manager.  This convention gives the manager a chance to do
    %   internal accounting and any other system-level coordination.
    %   - Users should not call managed object methods directly.  Instead,
    %   users should call manager methods, which in turn may call object
    %   methods.  The dotsAllObjectManagers with "Method" in their names
    %   are the most direct way to access managed object methods.  This
    %   convention allows managers to do system-level coordination which
    %   could actually involve calling a method on a different object!
    %   - Users should set and get managed object properties normally, with
    %   "." notation.  Users may also use the set* and get* methods of a
    %   manager, when convenient.
    %   .
    %   It is up to you, the user, to carry out the usage parts of the
    %   contract.
    % @details
    % <b>Coordination</b>
    % @details
    % A manager's job is to coordinate simpler behaviors and make
    % high-level decisions.  These would be impractical or redundant to
    % implement for each type of managed object.  For example:
    %   - The manager might need to report the creataion of a new object to
    %   a remote machine.
    %   - The manager might need to request object behaviors on a remote
    %   machine instead of the local machine.
    %   - The manager might decide to reuse an existing object instead of
    %   actually creating a new one.
    %   - The manager can group objects together for convenience, and get
    %   and set properties for whole groups at once.
    %   - The manager can call a method across a group objects all at
    %   once.  For instance, the mayDrawNextFrame() method of
    %   dotsTheDrawablesManager collects draw() method calls into
    %   one high-level behavior.
    %   .
    % @details
    % dotsAllObjectManagers itself is not a usable class, it just provides
    % a uniform interface and core functionality to subclasses (see below).
    % @ingroup foundataion
    
    properties
        % topsGroupedList of objects to coordinate
        % @details
        % Objects must be subclasses of dotsAllManagedObjects.  Each object
        % is listed under multiple groups:
        %   - All objects belong to a default group.
        %   - Each object belongs to a group with the same name as its
        %   class.
        %   - Each object may belong to additional user-specified groups.
        %   See the addObjectToGroup() method.
        %   .
        % The mnemonic for each object is its UID (unique identifier).  See
        % the dotsAllManagedObjects class.
        managedObjects;
        
        % function that returns the current time as a number
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        clockFunction;
        
        % string name for this machine, such as a UNIX hostname
        % @details
        % Automatically uses the hostName specified by
        % dotsTheMachineConfiguration.
        hostName;
    end
    
    properties (Hidden)
        % string name for the managedObjects group that contains every
        % object
        defaultGroup = 'all';
        
        % running count of UID serial numbers
        serialCount;
        
        % special UID 'self' refers to this manager instance
        UID = 'self';
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.serialCount = 0;
            self.initializeLists({'managedObjects'});
            dotsTheSwitchboard.sharePropertyOfObject('clockFunction', self);
        end
        
        % Managed objects should be created with this method.
        % @param className the string name of a dotsAllManagedObjects
        % subclass
        % @param size optional size of object array to create
        % @details
        % Returns an instance of @a className.  Adds the instance to
        % managedObjects in the default group and the @a className group.
        % @details
        % If @a size is provided, it should be a vector of dimension
        % lengths specifying the dimensions of an array of new objects to
        % create.  newObject() will return an array of @a className
        % instances and add each element to the default group and @a
        % className group.
        % @details
        % dotsAllManagedObjects objects should not be created with their
        % own constructors.  Instead, they should be created with the
        % newObject() method of the appropriate manager.  For example,
        % dotsDrawable subclasses should be created with the newObject()
        % method dotsTheDrawablesManager.
        % @details
        % newObject() allows the manager to do internal accounting and make
        % high-level decisions that are impractctical for individual
        % constructors.
        function object = newObject(self, className, size)
            if nargin < 3
                size = [1,1];
            end
            howMany = prod(size);
            
            UID = cell(1, howMany);
            for ii = 1:howMany
                UID{ii} = self.getUIDForClassName(className);
            end
            object = self.newObjectWithUID(className, size, UID);
        end
        
        % Stop managing an object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @details
        % Eliminates @a object from all groups in managedObjects.  @a
        % object will no longer coordinate with the manager.  If no other
        % workspace contains @a object, Matlab may delete it.
        function removeObject(self, object)
            for g = object.groups
                self.removeFromList(object, g{1});
            end
        end
        
        % Add an object to a custom group.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should join
        % @details
        % @param mnemonic list mnemonic to use instead of @a object.UID
        % @details
        % Adds @a object to the @a group in the managedObjects list.  @a
        % object may join any number of custom groups.  If @a mnemonic is
        % given, it must be of the same type (string or numeric) as other
        % mnemonics in @a group.  Items @a group will be sorted by @a
        % mnemonic.
        function addObjectToGroup(self, object, group, mnemonic)
            if nargin < 4
                mnemonic = object.UID;
            end
            self.addToList(object, group, mnemonic);
        end
        
        % Remove an object from a custom group.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should quit.
        % @details
        % removes @object from the @a group group of managedObjects.
        % Objects should only quit user-specified, custom groups.  They
        % should not attempt to quit the default group or the group for
        % their class name.
        function removeObjectFromGroup(self, object, group)
            self.removeFromList(object, group);
        end
        
        % Set one property for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @param value the value to assign to @a property
        % @details
        % setObjectProperty() has the same effect on @a object as setting
        % its @a property directly, except that @a object will not report
        % the change to its manager (which would be redundant).
        function setObjectProperty(self, object, property, value)
            object.setPropertySilently(property, value);
        end
        
        % Set multiple properties for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param varargin pairs of string property names and values to set
        % to @a object.
        % @details
        % Sets multiple properties at once.  Allows the manager to
        % coordinate property changes in one batch rather than one at a
        % time.
        % @details
        % For each property-value pair, setObjectPropertiesMany() has the
        % same effect on @a object as setting its property directly, except
        % that @a object will not report the change to its manager (which
        % would be redundant).
        function setObjectPropertiesMany(self, object, varargin)
            for ii = 2:2:length(varargin)
                property = varargin{ii-1};
                value = varargin{ii};
                object.setPropertySilently(property, value);
            end
        end
        
        % Get one property value from one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @details
        % Returns the value of the given @a property of @a object.
        % @details
        % getObjectProperty() is equivalent to getting the @a property of
        % @a object directly.
        function value = getObjectProperty(self, object, property)
            value = object.(property);
        end
        
        % Set one property for multiple objects.
        % @param group one of the group names in managedObjects
        % @param property the string name of a property shared by all the
        % objects in @a group.
        % @param value the value to assign to @a property
        % @details
        % Sets a property across a group of objects at once.  Allows the
        % manager to coordinate property changes in one batch rather than
        % one at a time.
        % @details
        % For each object in @a group, setGroupProperty() has the same
        % effect as setting the object's property directly, except the
        % object will not report the change to its manager (which would be
        % redundant).
        function setGroupProperty(self, group, property, value)
            objects = self.getGroup(group);
            for ii = 1:length(objects)
                objects{ii}.setPropertySilently(property, value);
            end
        end
        
        % Call a method on one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param method the string name of a method of @a object
        % @param varargin optional arguments to pass to @a method.
        % @details
        % callObjectMethod() is equivalent to calling the given @a method
        % of @a object, with any given arguments, directly.
        function callObjectMethod(self, object, method, varargin)
            object.(method)(varargin{:});
        end
        
        % Call a method on one object and assign the return value.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param method the string name of a method of @a object
        % @param returnProperty the string name of a property of @a object
        % which will receive the return value from @a method
        % @param varargin optional arguments to pass to @a method
        % @details
        % Invokes the given @a method of @a object, with any given
        % arguments, and assigns the result to the given @a returnProperty
        % of @a object.
        % @details
        % @a object will not report the property change to its manager
        % (which would be redundant).
        function value = getObjectMethodResult(self, object, method, returnProperty, varargin)
            value = object.(method)(varargin{:});
            object.setPropertySilently(returnProperty, value);
        end
        
        % Call a method on multiple objects.
        % @param group one of the group names in managedObjects
        % @param method the string name of a method shared by all the
        % objects in @a group.
        % @param varargin optional arguments to pass to @a method.
        % @details
        % Invokes the given @a method, with any given arguments, across a
        % group of objects at once.  Allows the manager to coordinate
        % the method calls in one batch rather than one at a time.
        function callGroupMethod(self, group, method, varargin)
            objects = self.getGroup(group);
            for ii = 1:length(objects)
                objects{ii}.(method)(varargin{:});
            end
        end
        
        % Get a group of managed objects.
        % @param group one of the group names in managedObjects
        % @details
        % Returns all of the objects in @a group in a cell array.  Also
        % returns, as an optional second output, a cell array containing
        % the unique identifier list mnemonic for each object.
        function [objects, UIDs] = getGroup(self, group)
            [objects, UIDs] = self.managedObjects.getAllItemsFromGroup(group);
        end
        
        % Get all of the managed objects.
        % @details
        % Returns all of the objects in managedObjects in a cell array.
        % Also returns, as an optional second output, a cell array
        % containing the unique identifier list mnemonic for each object.
        function [objects, UIDs] = getAll(self)
            if self.managedObjects.length > 0
                [objects, UIDs] = self.managedObjects.getAllItemsFromGroup(self.defaultGroup);
            else
                objects = {};
                UIDs = {};
            end
        end
        
        % Managed objects must report property changes here.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of the property of @a object that
        % recently changed.
        % @details
        % Users don't need to use this function.  Rather,
        % dotsAllManagedObjects objects use this function to report when
        % their properties change, in case the manager needs to coordinate
        % the change with other objects.
        % @details
        % By default, this function does nothing.  Subclasses may override
        % it to do coordination.
        function acknowledgePropertyChange(self, object, property)
        end
    end
    
    methods (Access=protected)
        % Make a unique identifier string for a managed object (used
        % internally).
        function UID = getUIDForClassName(self, className)
            h = self.hostName;
            self.serialCount = self.serialCount + 1;
            UID = sprintf('%s:%s:%d', h, className, self.serialCount);
        end

        % Get the object that has the given UID (used internally).
        % @details
        % The special UID 'self' refers to this manager instance.
        function object = getObjectByUID(self, UID)
            if isempty(UID) || strcmp(UID, 'self')
                object = self;
            else
                object = self.managedObjects.getItemFromGroupWithMnemonic( ...
                    self.defaultGroup, UID);
            end
        end
        
        % Make a new object with the given UID (used internally).
        function object = newObjectWithUID(self, className, size, UID)
            howMany = prod(size);
            constructor = str2func(className);
            object(howMany) = feval(constructor);
            for ii = 1:howMany
                object(ii).UID = UID{ii};
                self.addToList(object(ii), self.defaultGroup);
                self.addToList(object(ii), className);
                object(ii).startReportingPropertiesToManager(self);
            end
            object = reshape(object, size);
        end
        
        % Add an object to a group (used internally).
        function addToList(self, object, group, mnemonic)        
            if nargin < 4
                mnemonic = object.UID;
            end    
            self.managedObjects.addItemToGroupWithMnemonic( ...
                object, group, mnemonic);
            isInGroup = strcmp(object.groups, group);
            if ~any(isInGroup)
                object.groups{end+1} = group;
            end
        end
        
        % Eliminate an object from a group (used internally).
        function removeFromList(self, object, group)
            self.managedObjects.removeMnemonicFromGroup(object.UID, group);
            isInGroup = strcmp(object.groups, group);
            if any(isInGroup)
                object.groups = object.groups(~isInGroup);
            end
            
            if isempty(object.groups)
                object.stopReportingPropertiesToManager;
            end
        end
    end
end